/*******************************************************************************
 * Copyright 2007-2008 Vidar Svansson
 *
 * This file is part of JAdapter.
 *
 * JAdapter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JAdapter is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with JAdapter.  If not, see <http://www.gnu.org/licenses/>.
 *******************************************************************************/

package org.jadapter;

import org.jadapter.adapters.IterableString;
import org.jadapter.exceptions.AdaptationException;
import org.jadapter.func.Filter;
import org.jadapter.func.JoinedIterator;
import org.jadapter.registry.AdapterRegistry;
import org.jadapter.registry.TransformerRegistry;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

/**
 * A handy iterable class.
 * 
 * @author Vidar Svansson
 *
 * @param <T> Generic type of the {@link Iterable} set.
 *
 * @see Iterable
 * 
 * @since 0.1
 */
public class Query<T> implements Iterable<T> {

    private Iterable<? extends T> iterable;
    private Filter<T> filter;
    private int length = -1;

            
    public Query(T ... t) {
        this(null, Arrays.asList(t));
    }

    /**
     * Create an iterable String.
     *
     * @deprecated This is not type-safe so it probably shouldn't be used.
     * User Query.$(String) instead for type-safety.
     *
     *
     * @param string the String to iterate.
     */
    @SuppressWarnings("unchecked")
    public Query (String string) {
        this.iterable = (Iterable<T>) new IterableString(string);
    }


    public Query(Iterable<? extends T> iterable) {
        this(null, iterable);
    }
    

    private Query(Filter<T> filter, Iterable<? extends T> iterable){
        this.iterable = iterable;
        this.filter = filter;
    }
    

    /** 
     * Returns an iterator over the elements in this query.
     * Only elements that pass the filter are returned if the query contains a filter.
     * <br></br><br></br>
     * @see Iterable#iterator
     * @see Filter
     * @since 0.1
     */
    public Iterator<T> iterator() {
        Filter<T> filter = filter();
        return filter == null ? 
            (Iterator<T>)iterable.iterator() :
            new QueryIterator(iterable, filter(), this);
    }
    
    /** 
     * Chain this Query with a new filter.
     * 
     * @param filter to be applied to the query.
     * @return a chained Query object containing the filter.
     * @since 0.1
     */
    final public Query<T> filter(Filter<T> filter)  {
        return new Query<T>(filter, this);
    }

    protected Filter<T> filter(){
        return filter;
    }
        
    /** 
     * Slice the query by a given range.
     * 
     * @param x the start index 
     * @param y the end index
     * @return a chained query that iterates from x to y
     * @since 0.1
     */
    final public Query<T> slice(final int x, final int y) {
        return new Query<T>(this) {
            @Override protected Filter<T> filter() {
                return new Filter<T>() {
                    int state = 0;
                    public boolean filter(T t) {
                        return ++ state > x && state < y ;
                    }    
                };
            }           
        };
    }
    
    /** 
     * Slice the query from a given index
     * 
     * @param x the start index 
     * @return a chained query that iterates from x
     * @since 0.1
     */   
    final public Query<T> slice(final int x) {
        return new Query<T>(this) {
            @Override protected Filter<T> filter() {
                return new Filter<T>() {
                    int state = 0;
                    public boolean filter(T t) {
                        return ++ state > x ;
                    }     
                };
            }           
        };        
    } 

    /** 
     * Return the length of this query.
     * If the internal object is a {@link Collection}, then return its size.
     * If the query has never been iterated, then an iteration will be performed.
     * <br></br><br></br>
     * @return the length of this query.
     * @since 0.1
     */
    public int length() {
        if (length > -1) return length;
        if (iterable instanceof Collection) {
            return ((Collection<T>)iterable).size();
        }
        int tmp = 0; 
        for (T t : this) ++ tmp;
        return length = tmp;
    }    
    
    /**
     * Returns an element in the query by the given index.
     * Currently returns null if not found.
     * <br></br><br></br>
     * @param index
     * @return an element in the query by the given index
     * @since 0.1
     */
    public T get(final int index) {
        int x = 0;
        for (T t : this) {
            if (x ++ == index) {
                return t;
            }
        } return null;
    }

    /**
     * Create a new Query of elements represented by this
     * Query transformed by a transformer registry
     * <br></br><br></br>
     * @param <I> The Generic type of the returned Query.
     * @param transformers the transformer registry to use in the transformation.
     * @param toClass the type to transform objects to.
     * @return A new Query object containing the transformed elements.
     * @since 0.4
     */
    final public <I> Query<I> map(TransformerRegistry transformers, Class<I> toClass) {
        List<I> map = new LinkedList<I>();
        for (T t : this) {
            map.add(transformers.transform(t, toClass));
        }
        return new Query<I>(map);
    }

    /**
     * Create a new Query of elements represented by this 
     * Query transformed by the transformer.
     * <br></br><br></br>
     * @param <I> The Generic type of the returned Query.
     * @param transformer the transformer to apply on the elements.
     * @return A new Query object containing the transformed elements.
     * @since 0.1
     */
    final public <I> Query<I> map(Transformer<I,T> transformer) {
        List<I> map = new LinkedList<I>();
        for (T t : this) { map.add(transformer.transform(t)); }  
        return new Query<I>(map);
    }  
    
    /** 
     * A wrapper around the for(T : Iterable<T>) syntax for chaining 
     * @param lambda the function to apply to each element
     * 
     * @return a reference to this object.
     * @since 0.1
     */
    final public Query<T> each(Filter<T> lambda) {
        for (T t : this) lambda.filter(t);
        return this;
    }      
    
    /**
     * A wrapper around method invocation on each element of the query.
     * When invoked, a proxy of type T is returned. 
     * The proxy can then be used to invoke a method on each element.
     * This works best with void methods since it is not possible to return all the results.
     * This currently only works on queries that actually contain at least one element.
     * 
     * 
     * @return a T {@link Proxy} that when invoked, invokes the method on all elements in this query.
     * @since 0.1
     */
    final public T each() {
    	// create proxy of T
    	// when proxy method is invoked, invoke on all elements
    	Class<?>[] interfaces = interfaces(false);
    	@SuppressWarnings("unchecked")
        T t = (T)Proxy.newProxyInstance(this.getClass().getClassLoader(), interfaces,
				new IteratorInvocationHandler<T>(this));

        return t;
    }    
    
    public Object compose() {
    	Class<?>[] interfaces = interfaces(true);  	   	
    	return Proxy.newProxyInstance(this.getClass().getClassLoader(), interfaces, 
    			new CompositeInvocationHandler<T>(this));  	
    }
    
    private final class CompositeInvocationHandler<C> implements InvocationHandler {
    	
    	public Query<C> query;
    	
    	public CompositeInvocationHandler(Query<C> query) {
    		this.query = query;
    	}
		
		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			for(C c : query){	        	
	        	if(method.getDeclaringClass().isInstance(c))        	
	        		return method.invoke(c, args);
			}
			throw new RuntimeException("found nothing");
		}    	
    	
    }  
    
    private Class<?>[] interfaces(boolean all){
    	if(!all){
    		Iterator<T> iterator = iterator();
    		if(iterator.hasNext()){
    			T first = iterator.next();
    			return first.getClass().getInterfaces();
    		}
    	}
    	  	
		Collection<Class<?>> interfaces = new HashSet<Class<?>>();
		for(T t : this) {
			Class<?>[] ifaces = t.getClass().getInterfaces();
			
			for (Class<?> iface : ifaces) {
				interfaces.add(iface);
			}    			
		}
				
		return interfaces.toArray(new Class<?>[interfaces.size()]);   	
    }    
   
    
    
    final public <E extends Exception> T xor(Class<E> ... catchables) throws Exception {
    	Class<?>[] interfaces = interfaces(false);
        return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(),  interfaces, 
                new XorInvocationHandler<T,E>(this, new Query<Class<E>>(catchables)));            
    }
    
    private final class XorInvocationHandler<C, E extends Exception> implements InvocationHandler {
        
        public Query<C> query;
        Query<Class<E>> catchables;
        
        public XorInvocationHandler(Query<C> query, Query<Class<E>> catchables) {
            this.query = query;
            this.catchables = catchables;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            Exception lastException = null;

            for(C c : query) {
                try {                 
                    return method.invoke(c, args);

                } catch (Exception e1) {
                    lastException = e1;
                    for(Class<E> catchable : catchables) {
                        if(catchable.isInstance(e1)){
                            lastException = null;
                        }                   
                    }
                }  
            }        

            if(lastException != null) throw lastException;
            throw new AssertionError("unreachable state");
        } 

    }
    
    
    private static final class IteratorInvocationHandler<C> implements InvocationHandler {
    	
    	public Query<C> query;
    	
    	public IteratorInvocationHandler(Query<C> query) {
    		this.query = query;
    	}
		
		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			for(C c : query) method.invoke(c, args);					
			return null;
		}    	
    	
    }    

    private final class QueryIterator implements Iterator<T> {
        private int length = 0;
        private Iterator<? extends T> iterator;
        
        private Filter<T> filter;
        private boolean hasNext = false;
        private T next = null;
        private Query<T> query;

        public QueryIterator(Iterable<? extends T> iterable, Filter<T> filter, Query<T> query) {
            this.iterator = iterable.iterator();
            this.filter = filter;
            this.query = query;
            forward();
        }
        
        private void forward() {
            hasNext = false;
            T current;            
            while (!hasNext && iterator.hasNext()) {
                current = iterator.next();
                if (filter == null || filter.filter(current)) {
                    ++ length;

                    next = current;                    
                    hasNext = true;
                }
            }
            if (!iterator.hasNext()) query.length = length;    
        }
        
        public T next() {
            T ret = next;
            forward();
            return ret;
        }

        public boolean hasNext() {
            return hasNext;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }      
    }  
    
    
    /**
     * Prepend a set to this query. 
     * @param iterable the set to prepend
     * @return a chained Query consisting of elements the joined sets.
     * @since 0.1
     */
    public Query<T> prepend(final Iterable<? extends T> iterable){        
        return new Query<T>(this) {
            @Override public Iterator<T> iterator(){
                return new JoinedIterator<T>(iterable.iterator(), super.iterator());
            }
        };
    } 
        
    /**
     * Append a set to this query. 
     * @param iterable the set to append
     * @return a chained Query consisting of elements the joined sets.
     * @since 0.1
     */    
    public Query<T> append(final Iterable<? extends T> iterable) {        
        return new Query<T>(this) {            
            @Override public Iterator<T> iterator(){
                return new JoinedIterator<T>(super.iterator(), iterable.iterator());
            }
        };
    }


    public static <T> Query<T> NEW(T ... t){
        return new Query<T>(t);
    }
    
    public static <T> T[] array(T ... t){
        return t;
    }

    public static <T> List<T> list(T ... t){
        return Arrays.asList(t);
    }
    
    
    public static <T> Query<T> $(T ... t) {
    	return new Query<T>(t);
    }


    public static <T> Query<T> $(Iterable<T> iterable) {
    	return new Query<T>(null, iterable);
    }

    public static Query<String> $(String string) {

        Iterable<String> iterable = new IterableString(string);

        return new Query<String>(iterable);

    }
    
}